/**
 * 
 */
package com.redxiii.spike.util.command;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.SubnodeConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
import org.slf4j.Logger;

import com.redxiii.spike.api.command.Command;
import com.redxiii.spike.api.command.CommandFactory;
import com.redxiii.spike.api.command.MsgInterpreter;
import com.redxiii.spike.util.core.StorageSystem;

/**
 * @author Daniel Filgueiras
 * 06/05/2011
 */
public final class CommandClassLoader extends ClassLoader {

	private static final Logger logger = org.slf4j.LoggerFactory.getLogger(CommandClassLoader.class);
	private static final String CMD_FILE_NAME = "Commands.xml";
	private HierarchicalConfiguration config;
	private Map<String,MsgInterpreter> interpreters = new HashMap<String,MsgInterpreter>();
	
	public CommandClassLoader(ClassLoader parent) {
		this(parent, CMD_FILE_NAME);
	}
	
	public CommandClassLoader(ClassLoader parent, String configurationFile) {
		super(parent);
		logger.debug("Configuration file: {}", configurationFile);
		config = (HierarchicalConfiguration) StorageSystem.getInstance().getConfiguration(configurationFile);
	}
	
	public Collection<MsgInterpreter> createInterpreters() {
		
		interpreters.clear();
		
		if (config != null) {
			parseConfiguration(config);
		}
		loadExternalLibrary();
		
		return interpreters.values();
	}
	
	@SuppressWarnings("unchecked")
	private void parseConfiguration(HierarchicalConfiguration config) {
		
		List<SubnodeConfiguration> packNodes = config.configurationsAt("package");
		
		logger.debug("Found {} packages", packNodes.size());
		
		for (SubnodeConfiguration packNode : packNodes) {
			String pack = (String) packNode.getProperty("[@id]");
		
			List<SubnodeConfiguration> cmdNodes = packNode.configurationsAt("command");
			
			logger.debug("Found {} commands", cmdNodes.size());
			
			for (SubnodeConfiguration cmdNode : cmdNodes) {
				String cmd = (String) cmdNode.getProperty("[@id]");
				String enabled = (String) cmdNode.getProperty("[@enable]");
				if (enabled != null && enabled.equals("false")) {
					logger.warn("Command \"{}\" is disabled", cmd);
				} else {
					loadCommand(pack + "." + cmd, cmdNode);
				}
				
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private void loadCommand(String className, HierarchicalConfiguration configRef) {
		try {
			Class<? extends Command> commandClass = (Class<? extends Command>) this.loadClass(className);
			
			CommandFactory factory = commandClass.getAnnotation(CommandFactory.class);
			
			if (factory != null && factory.value() != null) {
				Class<? extends MsgInterpreter> interpreterClass = factory.value();
				
				MsgInterpreter interpreter = interpreters.get(interpreterClass.getName());
				if (interpreter == null) {
					interpreter = interpreterClass.newInstance();
					interpreters.put(interpreterClass.getName(), interpreter);
				}
				
				interpreter.add(commandClass, configRef);
			} else {
				logger.warn("Command class '{}' is not annotated with @CommandFactory", className);
			}
		} catch (Exception e) {
			logger.debug("Programming error command {}! Let's dismiss someone immediately!!!", className, e);
		}
	}
	
	public void loadExternalLibrary() {
		
		File dir = new File("./external/");
		if (dir.exists() && dir.isDirectory()) {
			for (File file : dir.listFiles()) {
				if (file.getName().endsWith(".jar")) {
					try {
						loadCommand(Command.class, file);
						HierarchicalConfiguration configRef = new XMLConfiguration("./external/" + file.getName().replace(".jar", ".xml"));
						
						parseConfiguration(configRef);
						
					} catch (ConfigurationException e) {
						logger.debug("Error o external jar configuration file {}", file, e);
					}
				}
			}
		}
	}
	
	private <T> List<Class<? extends T>> loadCommand(Class<T> classType, File folder) {
		
		List<Class<? extends T>> commandClasses = new ArrayList<Class<? extends T>>();
		
		List<Class<?>> classes = parseJarFile(folder);
		logger.debug("Obtained {} classes of \"{}\"", classes.size(), folder);
		
		for (Class<?> clazz : classes) {
			if (classType.isAssignableFrom(clazz)) {
				logger.debug("Command found \"{}\"", clazz.getCanonicalName());
				clazz.asSubclass(classType);
				commandClasses.add(clazz.asSubclass(classType));
			}
		}
		
		return commandClasses;
	}
	
	@SuppressWarnings("unchecked")
	private List<Class<?>> parseJarFile(File jarFile) {
		
		ZipFile zipFile = null;
		try {
			zipFile = new ZipFile(jarFile);
		} catch (ZipException e) {
			logger.error("Error reading Zip/Jar \"{}\"", jarFile, e);
			return Collections.EMPTY_LIST;
		} catch (IOException e) {
			logger.error("Error reading Zip/Jar \"{}\"", jarFile, e);
			return Collections.EMPTY_LIST;
		}
		
		Enumeration<? extends ZipEntry> entries = zipFile.entries();
		
		List<Class<?>> classes = new ArrayList<Class<?>>();
		
		while (entries.hasMoreElements()) {
			ZipEntry entry = entries.nextElement();
			
			if (entry.isDirectory())
				continue;
			
			try {
				if (entry.getName().endsWith(".class")) {
					InputStream stream = zipFile.getInputStream(entry);
					Class<?> clazz = parseClass(stream, entry.getName());
					classes.add(clazz);
				}
			} catch (IOException e) {
				logger.error("Error jar entry \"{}\"", entry.getName(), e);
			}
		}
		
		return classes;
	}
	
	private Class<?> parseClass(InputStream stream, String fileName) {
		
		try {
			byte[] bClass = new byte[stream.available()];
			stream.read(bClass, 0, bClass.length);
			
			String name = fileName
					.substring(0, fileName.length() - 6)
					.replace('/', '.');
			
			return defineClass(name, bClass, 0, bClass.length);
		} catch (IOException e) {
			logger.error("Error reading class \"{}\"", fileName, e);
		} catch (ClassFormatError e) {
			logger.error("Error loading class \"{}\"", fileName, e);
		} catch (IndexOutOfBoundsException e) {
			logger.error("Error loading class \"{}\"", fileName, e);
		} catch (SecurityException e) {
			logger.error("Error loading class \"{}\"", fileName, e);
		}
		return null;
	}
}
